@陈逸云 吴怡:《古籍文献中人名实体信息的提取方法及实现研究》
摘要
- 核心问题:古籍中人名实体提取困难重重,主要在于称呼多样、重名率高、与常用词混淆、指代简化、资料缺失等。
- 解决方案:提出一种结合分类词典树分词和 FAISS 向量检索消歧的方法。
- 研究成果:
- 在大规模古籍语料上,实现三字名识别准确率约 92%,二字名约 85%。
- 自动发现约 6.8万 个新人物。
- 开发了可视化应用,展示人物关系与影响网络。
引言
- 核心挑战:在古籍知识图谱构建中,人名的提取是时间、地点、人名三要素中最具挑战性的。
一、 提取人名信息的若干难题
- (一) 称呼多样性
- 组合方式:姓名、字、号、行第、官爵、籍贯、谥号等多种信息组合。
- 简化形式:
- 上下文简化:如“项籍”后文简称“籍”。
- 格律简化:诗词中为满足字数限制而缩减,如“钟子期”省作“钟期”。
- 年号代称:以年号指代皇帝,如“嘉靖皇帝”代指明世宗。
- (二) 高重名率与消歧难度
- 现象:重名、同字、同号现象极为普遍。
- 数据:研究库中 70.4% 的人物存在至少一种称呼与他人相同。
- 策略:使用更长的、组合式的称呼(如姓名+籍贯)可降低重名率。
- (三) 名号与常用词汇混淆
- 例子:黄庭坚号“山谷”,文徵明号“衡山”,“兰陵王”既是封号也是词牌名。
- (四) 姓氏与官爵组合歧义
- 例子:“济南王太傅”,可理解为“济南王府的太傅”或“济南的王姓太傅”。
- (五) 称字、号需社交关系背景
- 例子:陆游诗中称“无咎”,需知其与韩元吉(字无咎)交往密切才能正确消歧。
- (六) 需具备新名发现能力
- 解决方案不应局限于已有人物库,必须能识别未收录的人名。
二、 解决方案:分类词典树Trie分词 + FAISS消歧
-
核心思路:
- 分词与候选识别:通过多领域本体库(人物、职官、地理等)构建 Trie 树 (分类词典树),进行文本分词,高效识别出候选实体。
- 匹配与消歧:调用 FAISS 向量相似度检索,结合上下文信息,在人物本体库中找到最佳匹配实体,实现精准消歧。
-
(一) 消歧方法
- 1. 借助人物本体属性
- 时间信息:利用作者生卒年、作品朝代,排除时间线不符的人物。
- 地理信息:籍贯作为关键特征,辅助判断。
- 知名度:被提及次数多的名人(如杜甫字“子美”)拥有更高默认权重。
- 社交网络:作者与某人的“亲密关系”(如共同创作、唱和)是强消歧证据。
- 易混淆度:通过计算称呼在人物出生前的使用频率,评估其与常用词的混淆程度。
- 典故权重:若称呼涉及典故,则提高其权重。
- 2. 借助实时反馈识别简化名
- 机制:识别出全名后,将其简称(如“籍”)临时动态加入Trie树,以便在紧邻的下文中识别。
- 3. 借助表达范式发现新名
- 机制:统计已标识人名周边的文本模式,形成“表达范式”(如
寄<人名>
、为<人名>赋
),利用这些范式在文本中发现新的人名。
- 机制:统计已标识人名周边的文本模式,形成“表达范式”(如
- 4. 其他消歧策略
- 称呼习惯:降低直呼其名(皇帝对臣子除外)的权重。
- 亲属关系:利用父子等亲属关系辅助消歧。
- 1. 借助人物本体属性
-
(二) 综合推断和优化
- 在完成局部消歧后,利用全文的时间线和人物关系网进行全局优化,解决剩余的疑难问题。
三、 借助实体提取技术构建人物本体库
- 核心关系:相辅相成,迭代优化。
- 实体提取技术,用于从人物传记、诗文等资料中自动化构建人物基础本体库。
- 完善的本体库,反过来提升实体提取的准确性与覆盖度。
- 本体库构成:
- 基本信息:姓、名、字、号、生卒、朝代、官职等。
- 扩展信息:知名度、易混淆度、亲密人物列表等。
- 辅助信息:含人名的表达范式、常用共现词等。
四、 应用开发
- 解决痛点:解决了传统关键词检索难以穷举所有称呼、易返回无关结果的问题。
- 核心功能:
- 称呼归一:能将“陶潜”、“陶靖节”、“五柳先生”等不同称呼指向同一人物。
- 影响力可视化:以图谱形式直观展示某位作家所提及的前人(追溯)与提及他的后人(影响)。
总结
- 核心贡献:提出了一套分类词典分词与FAISS向量检索相结合的古籍人名实体提取与链接的有效方案。
- 准确率:三字名称呼识别准确率约为 92%,二字名称呼约为 85%。若仅判断是否为人名而不进行实体链接,准确率可达 97%。
- 未来方向:
- 扩大人物库:持续收录更多历史人物。
- 增强关系消歧:借助 CBDB (中国历代人物传记资料库) 等外部数据库的人物关系数据,进一步提升消歧能力。
什么是 faiss
好的,当然可以。
FAISS (Facebook AI Similarity Search) 向量相似度检索的实现,本质上是为了解决一个核心问题:如何在海量(百万、十亿甚至更多)的高维向量中,快速找到与查询向量最相似的几个向量。
如果我们用“暴力搜索”(Brute-force Search)的方式,即把查询向量和数据库里的每一个向量都计算一次距离,那么当数据量巨大时,这个过程会慢得无法接受。FAISS 的精髓在于它使用了一系列**近似最近邻(Approximate Nearest Neighbor, ANN)**的算法,通过牺牲极小的精度来换取成百上千倍的检索速度。
下面,我将结合之前笔记中的“人名识别”案例,用一个通俗的比喻来分步解释其实现原理。
想象一下,你要在一个拥有一亿本藏书的巨大图书馆里,为你手上的一本书(查询向量)找到内容最相似的几本书(相似向量)。
核心前提:向量嵌入(Vector Embedding)
在检索之前,我们需要将所有信息都“翻译”成向量(一长串数字)。
- 数据库向量:对于人物库中的每一个人物(比如历史上的白居易),系统会根据他的各种属性(姓名“白居易”、字“乐天”、朝代“唐朝”、官职“左拾遗”、密友“元稹”、籍贯等)生成一个唯一的向量。这个向量在多维空间中的位置,就代表了“白居易”这个人物的“语义”。
- 查询向量:当系统在古籍中读到“乐天居士”这个词时,它也会根据这个词本身以及它的上下文(例如,这篇文章的作者是谁、创作于哪个朝代、附近是否提到了“元稹”或“江州司马”等)生成一个临时的查询向量。
目标:在代表了一亿个人物信息的向量库中,快速找到与“乐天居士”这个查询向量距离最近的那个向量,从而判断“乐天居士”指的就是“白居易”。
FAISS 的两步核心实现策略
FAISS 主要通过 “聚类” 和 “量化” 两大技术来加速检索。
第 1 步:聚类(Clustering) - 建立索引,缩小搜索范围
暴力搜索相当于在一亿本书中一本一本地翻。FAISS 的第一步是先给图书馆分区和编目。
-
实现方法:使用像 k-means 这样的聚类算法,将所有向量分成几千或几万个簇(Cluster)。每个簇就像是图书馆里的一个**“分区”,比如“唐代诗人区”、“宋代官员区”等。FAISS 会为每个簇计算一个中心点(质心),并建立一个倒排文件索引 (Inverted File Index, IVF)**。这个索引记录了每个向量属于哪个簇。
-
检索过程:
- 当一个查询向量(“乐天居士”)进来时,FAISS 不会去跟所有一亿个向量比较。
- 它首先将查询向量与几千个“分区”的中心点进行比较,快速找出距离最近的几个分区(比如,系统判断它最可能属于“唐代诗人区”和“唐代官员区”)。
- 然后,FAISS 只在这几个被选中的分区内部进行精确的搜索。
-
效果:搜索范围从一亿个向量,急剧缩小到了可能只有几万个向量,速度大大提升。这就是
IVF
(Inverted File)系列索引的核心思想。
第 2 步:量化(Quantization) - 压缩数据,加速计算
即使范围缩小到了几万,每个向量本身可能依然很大(比如由 128 或 256 个浮点数组成),占用大量内存,且逐一比较仍然耗时。FAISS 的第二步是给每个分区里的书进行“内容摘要”。
-
实现方法:最核心的技术叫做乘积量化 (Product Quantization, PQ)。
- 向量切分:将一个高维向量(如 128 维)切成多段短的子向量(如 8 段,每段 16 维)。
- 构建码本:对每一段子向量,再次进行聚类,生成一个小的“码本”(Codebook),比如包含 256 个最具代表性的子向量。可以把这个码本理解为对这一小段信息的 256 种“标准描述”。
- 编码:用码本中某个“标准描述”的ID(0-255,一个字节)来代替原来的那段子向量。
- 压缩完成:最终,一个庞大的浮点数向量就被压缩成了一小串ID(例如,8个字节的ID),极大地减少了内存占用。
-
检索过程:在分区内部进行搜索时,FAISS 可以通过预先计算好的距离表,极快地估算出查询向量和这些被压缩后的向量之间的距离,而不需要进行完整的浮点数运算。
-
效果:内存占用降低了几个数量级,并且在分区内的最终比较速度也获得了极大提升。
总结:FAISS 的完整工作流
结合这两步,FAISS 最常用的索引类型 IVFADC
(Inverted File system with Asymmetric Distance Computation) 的工作流程如下:
-
离线建库:
- 对所有人物的向量进行 聚类(IVF),建立分区。
- 对每个向量进行 量化(PQ),将其压缩并存入相应的分区。
-
在线查询 (当遇到“乐天居士”时):
- 拿到“乐天居士”的原始查询向量。
- 第一步筛选:将查询向量与所有分区的中心点比较,快速定位到最相关的几个分区。
- 第二步筛选:在选定的分区内,将原始查询向量与分区内所有被压缩过的向量进行快速距离估算。
- 返回结果:从估算结果中,返回距离最近的 Top-K 个向量的ID(比如,“白居易”的ID)。
通过**聚类(缩小搜索空间)和量化(降低存储和计算成本)**的巧妙结合,FAISS 实现了在牺牲极小(通常可忽略不计)精度的情况下,对海量高维向量进行超高速的相似度检索,这使其成为解决类似古籍人名消歧等大规模实体链接问题的理想工具。
词向量从哪来?
这是一个非常棒的问题!我们已经知道了FAISS是向量的“搜索引擎”,那么这些能代表词语和实体意义的向量,究竟是如何被“创造”出来的呢?
这个过程被称为词嵌入(Word Embedding),它是现代自然语言处理(NLP)的基石。其核心思想源于一句名言:
"You shall know a word by the company it keeps." (你将通过一个词的上下文来了解它。)
简单来说,如果两个词经常出现在相似的语境中,那么它们的向量在多维空间中也应该彼此靠近。例如,在大量文本中,“猫”和“狗”都经常与“宠物”、“食物”、“奔跑”等词一起出现,所以它们的向量会很相似。
下面我们来看看生成这些向量的主流方法,从经典模型到现代模型。
1. 经典静态词向量模型 (Static Word Embeddings)
这类模型的特点是:一个词只有一个固定的向量,无论它在哪个句子中。
a) Word2Vec (Google, 2013)
这是最经典、最广为人知的模型。它通过一个简单的神经网络,在海量文本上进行“预测任务”的训练,而我们真正想要的,是训练过程中产生的“副产品”——词向量。它主要有两种训练方式:
-
CBOW (Continuous Bag-of-Words) - 根据上下文猜中间的词
- 任务:把一个词的上下文(比如前后各两个词)作为输入,预测这个中心词是什么。
- 比喻:做“完形填空”。比如,看到句子
“一只___正在追老鼠”
,模型需要猜出中间的词很可能是“猫”。 - 过程:为了完成这个任务,模型必须学会哪些词的上下文是相似的。在这个过程中,它就为每个词学习到了一个能代表其“角色”和“意义”的向量。
-
Skip-gram - 根据中间的词猜上下文
- 任务:与CBOW相反,输入一个中心词,预测它周围的上下文可能出现哪些词。
- 比喻:给出“猫”,模型需要猜出它周围可能出现“一只”、“追”、“老鼠”、“宠物”等词。
- 效果:在大型语料库上,Skip-gram通常能学到比CBOW更高质量的词向量。
b) GloVe (Global Vectors for Word Representation) (Stanford, 2014)
Word2Vec利用的是局部上下文信息(滑动窗口),而GloVe则试图结合全局的统计信息。
- 核心思想:它不直接在句子上做预测,而是先统计一个巨大的词-词共现矩阵(Co-occurrence Matrix),记录任意两个词在特定窗口内一起出现了多少次。
- 训练目标:然后,它训练词向量,使得向量的点积结果能够尽可能地拟合这两个词共现频率的对数。
- 效果:GloVe生成的向量在词语类比等任务上表现非常出色。例如,它能学到
vector('国王') - vector('男人') + vector('女人')
在向量空间中非常接近vector('女王')
。
2. 现代动态/上下文词向量模型 (Contextual Word Embeddings)
经典模型的最大缺点是“一词一义”。比如“bank”在“river bank”(河岸)和“investment bank”(投资银行)中,会得到完全相同的向量,这显然是不对的。现代模型解决了这个问题。
BERT (Bidirectional Encoder Representations from Transformers) (Google, 2018)
BERT是革命性的模型,它开启了NLP的新时代。
- 核心思想:一个词的向量不是固定的,而是根据它所在的整个句子动态生成的。它使用了强大的 Transformer 架构,能够双向地理解一个词的上下文。
- 实现方式:
- 预训练 (Pre-training):BERT在海量的通用文本(如维基百科)上进行两种“游戏”来学习语言:
- Masked Language Model (MLM):随机遮盖(mask)句子中的一些词,然后让模型去预测被遮盖的词是什么。这比CBOW更强大,因为它能看到被遮盖词左右两边的所有信息。
- Next Sentence Prediction (NSP):给模型两个句子,让它判断第二句是不是第一句的下一句。这让模型学会了句子间的关系。
- 生成向量:当预训练完成后,你输入一个句子(例如
“乐天居士被贬江州”
),BERT会处理整个句子,然后为句子中的每一个词(“乐天”、“居士”、“被贬”...)都输出一个包含了当前语境信息的、独一无二的向量。此时,“乐天”的向量就已经知道了它和“江州”有关。
- 预训练 (Pre-training):BERT在海量的通用文本(如维基百科)上进行两种“游戏”来学习语言:
应用到古籍人名识别案例
回到我们最初的论文案例,系统是如何为“白居易”和在文中读到的“乐天居士”生成向量的呢?
- 语料库:首先,研究人员需要一个巨大的古籍语料库(比如《四库全书》、全唐诗、全宋词等)。
- 模型训练:他们会使用像BERT这样的模型,在这个古汉语语料库上进行预训练。这使得模型能够深刻理解古文的语法、词汇和知识。
- 生成实体向量:
- 对于人物库中的“白居易”:系统会将其所有信息(姓名、字号、官职、好友元稹、相关作品等)构成一个描述性文本,输入到训练好的BERT模型中,从而为“白居易”这个实体生成一个高质量的、聚合了所有信息的实体向量 (Entity Vector)。
- 对于文本中读到的“乐天居士”:系统会将包含“乐天居士”的整个句子或段落输入BERT,BERT会动态地为“乐天居士”这个词组生成一个查询向量 (Query Vector)。这个向量已经编码了它的上下文信息(比如,如果上文提到了“元稹”,这个向量就会和“元稹”的向量更接近)。
- 相似度检索:最后,将这个查询向量扔进FAISS,去和人物库中成千上万个实体向量进行比对,最终以极高的概率匹配到“白居易”的实体向量。
总结一下:词向量是通过让模型在海量文本中学习“词语的上下文用法”而产生的数学表示。经典模型(如Word2Vec)为每个词生成一个静态向量,而现代模型(如BERT)则能根据具体语境,为同一个词生成不同的动态向量,从而极大地提升了语言理解的准确性。
什么是分类词典树Trie分词
当然可以!这是一个非常核心的概念。我们来把它拆解成三个部分:分词、词典 和 分类词典树Trie,一步步解释清楚。
1. 什么是“分词” (Segmentation)?
对于英文这样的语言,单词之间有天然的空格作为分隔符,计算机处理起来很方便。
This is a sentence.
->["This", "is", "a", "sentence."]
但对于中文,句子是连续的字符流,没有明确的边界。
中国历代人物传记资料库
计算机不知道是该切分成 ["中国", "历代", "人物", "传记", "资料库"]
还是 ["中国", "历代", "人物", "传", "记", "资料", "库"]
。
分词,就是通过算法将连续的汉字序列切分成一个个具有独立意义的词语的过程。这是所有中文自然语言处理任务的第一步,也是最基础的一步。
2. 什么是“基于词典”的分词?
实现分词有很多方法,最直观的一种就是基于词典的分词。
- 思路:我们先准备一个非常庞大的词典(dictionary),里面包含了所有我们认为正确的词语(比如《新华词典》里的所有词)。
- 方法:拿到一个句子后,从左到右扫描,像查字典一样,看能匹配上词典里的哪些词。最简单的方法叫最大正向匹配法 (Forward Maximum Matching)。
- 例如,对于句子
“研究生命起源”
,词典里有“研究”
、“研究生”
、“生命”
、“起源”
。 - 从“研”开始,最长能匹配到词典里的词是
“研究生”
。好,切出一个词“研究生”
。 - 剩下
“命起源”
,从“命”开始,最长能匹配到的是“生命”
。切出“生命”
。 - 剩下
“起源”
,匹配成功。 - 结果:
["研究生", "生命", "起源"]
(这是一个错误的切分)。
- 例如,对于句子
这种简单方法的缺点显而易见:速度慢(每次都要在巨大的词典里查找),且容易出错(“贪心”地选择了最长的词,导致错误)。为了解决这个问题,就需要更高效的数据结构。
3. 什么是“分类词典树Trie”?
这就是论文中提到的解决方案。它不仅高效,而且更智能。
a) 什么是 Trie 树(字典树)?
Trie 树是一种专门为字符串查找而设计的树形数据结构。它非常适合用来实现一个高效的“词典”。
- 核心思想:一个词的每个字,都代表树上的一个节点。从根节点出发,沿着一条路径走到某个节点,这条路径就构成了一个词。
我们来构建一个简单的 Trie 树:
假设我们的词典里有三个词:{ “白居易”, “白傅”, “元稹” }
-
插入“白居易”:
Root -> 白 -> 居 -> 易
(在“易”节点上做一个标记,表示“白居易”是一个完整的词)
-
插入“白傅”:
- 路径
Root -> 白
已经存在了,不用新建。 - 从“白”节点出发,新建路径
-> 傅
(在“傅”节点上做一个标记,表示“白傅”是一个完整的词)
- 路径
-
插入“元稹”:
- 从
Root
出发,新建路径-> 元 -> 稹
(在“稹”节点上做一个标记)
- 从
构建出的 Trie 树大致如下:
(Root)
/ \
白 ------ 元
/ \ \
居 傅(词) 稹(词)
/
易(词)
使用 Trie 树分词的优势:
- 极速查找:对于句子
“白傅与元稹是好友”
,从第一个字“白”开始,沿着树向下走。当走到“傅”时,发现它是一个词的结尾,就找到了一个候选词“白傅”
。这个过程非常快,速度只跟词的长度有关,跟词典有多大无关! - 共享前缀:像
“白居易”
和“白傅”
共享了前缀“白”,在树中只存储一次,节省了空间。
b) 什么是“分类 (Classified)”?
这是论文中方法的精髓,它让 Trie 树变得更加强大。研究人员不仅仅是把词语放进树里,还给它们贴上了类别标签。
词典不再是单一的,而是分成了好几类:
- 人名库:{ 白居易, 白傅, 元稹, ... } -> 类别
[人名]
- 地名库:{ 长安, 洛阳, 江州, ... } -> 类别
[地名]
- 官职库:{ 刺史, 司马, 左拾遗, ... } -> 类别
[官职]
- 普通词汇库:{ 研究, 是, 和, ... } -> 类别
[普通词]
在构建 Trie 树时,当一个词结束时,不仅要做标记,还要把它的类别信息也存放在那个节点上。
(Root)
/ \
白 ------ 元
/ \ \
居 傅(词, [人名]) 稹(词, [人名])
/
易(词, [人名])
注意:一个词可以有多个类别标签,例如“兰陵王”既是[人名],也是[词牌名]。
总结:分类词典树Trie分词的工作流程
现在,我们把所有部分组合起来,看看它是如何工作的。
输入句子: “白傅在长安作诗”
- 扫描与匹配:程序从第一个字“白”开始,在分类 Trie 树中向下遍历。
- 识别候选与分类:
- 当走到路径
Root -> 白 -> 傅
时,程序发现“傅”节点被标记为 词尾,并且类别是[人名]
。 - 于是,它识别出一个候选词
“白傅”
,并立刻知道它是一个 人名。
- 当走到路径
- 继续扫描:
- 程序继续从“在”字开始扫描,匹配到普通词
“在”
。 - 接着从“长”开始,匹配到路径
Root -> 长 -> 安
,发现“安”节点被标记为 词尾,类别是[地名]
。 - 于是,识别出候选词
“长安”
,并知道它是一个 地名。
- 程序继续从“在”字开始扫描,匹配到普通词
- 输出与交接:
- 分词步骤的最终输出不再是简单的词语列表,而是 带有类别信息的结构化结果:
{ 词: "白傅", 类别: "人名" }
{ 词: "在", 类别: "普通词" }
{ 词: "长安", 类别: "地名" }
{ 词: "作诗", 类别: "普通词" }
- 分词步骤的最终输出不再是简单的词语列表,而是 带有类别信息的结构化结果:
- 启动下一步:当系统识别出一个类别为
[人名]
的词(如“白傅”)后,它就会把这个词和它的上下文信息,移交给下一个处理模块——也就是我们之前讨论的 FAISS 向量相似度检索系统,去完成最终的实体链接任务(即判断这个“白傅”具体是指哪个人——白居易)。
简而言之,分类词典树Trie分词是一种“一石二鸟”的高效策略:它在快速完成分词的同时,也完成了对词语的初步实体分类,为后续更复杂的AI任务(如实体链接)做好了数据准备,形成了一条清晰、高效的处理流水线。